/* code based jaxry, github, LGPL 3.0 */

function clamp(x, min, max) {
    return Math.min(max, Math.max(x, min));
  }
  
  function mod(x, n) {
    return ((x % n) + n) % n;
  }
  
  function copyPixelNearest(read, write) {
    const {width, height, data} = read;
    const readIndex = (x, y) => 4 * (y * width + x);
  
    return (xFrom, yFrom, to) => {
  
      const nearest = readIndex(
        clamp(Math.round(xFrom), 0, width - 1),
        clamp(Math.round(yFrom), 0, height - 1)
      );
  
      for (let channel = 0; channel < 3; channel++) {
        write.data[to + channel] = data[nearest + channel];
      }
    };
  }
  
  function copyPixelBilinear(read, write) {
    const {width, height, data} = read;
    const readIndex = (x, y) => 4 * (y * width + x);
  
    return (xFrom, yFrom, to) => {
      const xl = clamp(Math.floor(xFrom), 0, width - 1);
      const xr = clamp(Math.ceil(xFrom), 0, width - 1);
      const xf = xFrom - xl;
  
      const yl = clamp(Math.floor(yFrom), 0, height - 1);
      const yr = clamp(Math.ceil(yFrom), 0, height - 1);
      const yf = yFrom - yl;
  
      const p00 = readIndex(xl, yl);
      const p10 = readIndex(xr ,yl);
      const p01 = readIndex(xl, yr);
      const p11 = readIndex(xr, yr);
  
      for (let channel = 0; channel < 3; channel++) {
        const p0 = data[p00 + channel] * (1 - xf) + data[p10 + channel] * xf;
        const p1 = data[p01 + channel] * (1 - xf) + data[p11 + channel] * xf;
        write.data[to + channel] = Math.ceil(p0 * (1 - yf) + p1 * yf);
      }
    };
  }
  
  // performs a discrete convolution with a provided kernel
  function kernelResample(read, write, filterSize, kernel) {
    const {width, height, data} = read;
    const readIndex = (x, y) => 4 * (y * width + x);
  
    const twoFilterSize = 2*filterSize;
    const xMax = width - 1;
    const yMax = height - 1;
    const xKernel = new Array(4);
    const yKernel = new Array(4);
  
    return (xFrom, yFrom, to) => {
      const xl = Math.floor(xFrom);
      const yl = Math.floor(yFrom);
      const xStart = xl - filterSize + 1;
      const yStart = yl - filterSize + 1;
  
      for (let i = 0; i < twoFilterSize; i++) {
        xKernel[i] = kernel(xFrom - (xStart + i));
        yKernel[i] = kernel(yFrom - (yStart + i));
      }
  
      for (let channel = 0; channel < 3; channel++) {
        let q = 0;
  
        for (let i = 0; i < twoFilterSize; i++) {
          const y = yStart + i;
          const yClamped = clamp(y, 0, yMax);
          let p = 0;
          for (let j = 0; j < twoFilterSize; j++) {
            const x = xStart + j;
            const index = readIndex(clamp(x, 0, xMax), yClamped);
            p += data[index + channel] * xKernel[j];
  
          }
          q += p * yKernel[i];
        }
  
        write.data[to + channel] = Math.round(q);
      }
    };
  }
  
  function copyPixelBicubic(read, write) {
    const b = -0.5;
    const kernel = x => {
      x = Math.abs(x);
      const x2 = x*x;
      const x3 = x*x*x;
      return x <= 1 ?
        (b + 2)*x3 - (b + 3)*x2 + 1 :
        b*x3 - 5*b*x2 + 8*b*x - 4*b;
    };
  
    return kernelResample(read, write, 2, kernel);
  }
  
  function copyPixelLanczos(read, write) {
    const filterSize = 5;
    const kernel = x => {
      if (x === 0) {
        return 1;
      }
      else {
        const xp = Math.PI * x;
        return filterSize * Math.sin(xp) * Math.sin(xp / filterSize) / (xp * xp);
      }
    };
  
    return kernelResample(read, write, filterSize, kernel);
  }
  
  const orientations = {
    pz: (out, x, y) => {
      out.x = -1;
      out.y = -x;
      out.z = -y;
    },
    nz: (out, x, y) => {
      out.x = 1;
      out.y = x;
      out.z = -y;
    },
    px: (out, x, y) => {
      out.x = x;
      out.y = -1;
      out.z = -y;
    },
    nx: (out, x, y) => {
      out.x = -x;
      out.y = 1;
      out.z = -y;
    },
    py: (out, x, y) => {
      out.x = -y;
      out.y = -x;
      out.z = 1;
    },
    ny: (out, x, y) => {
      out.x = y;
      out.y = -x;
      out.z = -1;
    }
  };
  
  function renderFace({data: readData, face, rotation, interpolation, maxWidth = Infinity}) {
  
    const faceWidth = Math.min(maxWidth, readData.width / 4);
    const faceHeight = faceWidth;
  
    const cube = {};
    const orientation = orientations[face];
  
    const writeData = new ImageData(faceWidth, faceHeight);
  
    const copyPixel =
      interpolation === 'linear' ? copyPixelBilinear(readData, writeData) :
      interpolation === 'cubic' ? copyPixelBicubic(readData, writeData) :
      interpolation === 'lanczos' ? copyPixelLanczos(readData, writeData) :
      copyPixelNearest(readData, writeData);
  
    for (let x = 0; x < faceWidth; x++) {
      for (let y = 0; y < faceHeight; y++) {
        const to = 4 * (y * faceWidth + x);
  
        // fill alpha channel
        writeData.data[to + 3] = 255;
  
        // get position on cube face
        // cube is centered at the origin with a side length of 2
        orientation(cube, (2 * (x + 0.5) / faceWidth - 1), (2 * (y + 0.5) / faceHeight - 1));
  
        // project cube face onto unit sphere by converting cartesian to spherical coordinates
        const r = Math.sqrt(cube.x*cube.x + cube.y*cube.y + cube.z*cube.z);
        const lon = mod(Math.atan2(cube.y, cube.x) + rotation, 2 * Math.PI);
        const lat = Math.acos(cube.z / r);
  
        copyPixel(readData.width * lon / Math.PI / 2 - 0.5, readData.height * lat / Math.PI - 0.5, to);
      }
    }
  
    postMessage({data: writeData, faceName: face});
  }
  
  onmessage = function({data}) {
    if ( data.type && data.type.startsWith("panorama/")) {
      renderFace(data);
    } 
  };